前端现代包管理器的进化史
【招聘】字节跳动巨量星图/TCM团队持续招聘前端工程师,详情可见文后 / 字节跳动巨量星图/TCM团队招聘前端工程师
现在很多项目都会使用 pnpm 替代 npm or yarn 来做包管理,众所周知 pnpm 的宣传里有一个很吸引人的点是可以更小更快的安装我们的依赖。那么
为什么 pnpm 能做到更小更快呢? 除了更小更快他还解决了哪些问题让大家得以青睐呢?
要回答这些问题,就得了解这些年包管理的几场重大”变革“ ,于是便有了今天的分享。
Npm v1 & v2
刚开始 npm 的设计非常的简单:有一个标准的包管理器供大家下载和查阅,将开发人员从那个“网上下载资源,在手动解压添加近项目”的年代中解放出来。所以初版 npm 使用了很简单嵌套结构来进行版本管理。
我们假设有这样两个项目:
App1 依赖 packageA 和 packageC,而 packageA 和 packageC 都依赖了 packageB; App2 依赖 packageB 和 packageD,packageD 依赖 packageE,packageE 依赖 packageF......
那么这两个项目的 nodule_packages 目录结构如下:
这样做带来的问题也显而易见:
项目里会反复安装相同的依赖:比如 App1 下重复安装了 packageB 会带来依赖地狱:比如 App2 下的 D,E,F...... 不同的项目之间会重复安装相同的包:比如 App1 和 APP2 都安装了packageB
网上为此有一些经典的梗:
Npm v3
为了解决上述的问题,npm v3 完全重写了 npm 的安装程序,采用扁平化的方式,将主依赖项和子依赖都装到 node_modules 一级目录下,还是上面那个例子,此刻 node_modules 的目录结构如下:
这时项目内的重复安装和依赖地狱看似都得到了不错的解决,但此刻又带来了一些新的问题:
1. 不完全解决的重复依赖
我们假设 App1 又有了新的依赖 packageG 和 packageH,packageG、packageH 都依赖 packageB 的 v2.0 版本,那么此刻 node_modules 的依赖可能是以下结构:
由于A、C 和 G、H 依赖的是不同版本的 B,所以子依赖项提升时只会提升 v1.0 版本的 packageB,v2.0 版本的 packageB依旧挂在G,H下。显而易见,G 和 H 依旧安装了两份同样的 packageB v2.0。
2. 不确定性
发现我上述说:“node_modules的依赖 可能 是以下结构” ,因为 node_modules 的结构还可能是下面这种:
这个例子里 packageB v2.0 得到了提升,A 和C 下挂着 Bv1.0。
那么到底是提升 Bv1.0 还是提升 Bv2.0?取决于用户的安装顺序(install操作),这种不确定性为项目的依赖问题解决带来了极大的困难,不同的成员install的结果不一定是一样的......
3. 隐式依赖(又名幽灵依赖)
在上述的例子里,虽然 App1 没有直接声明引用 packageB,但项目里依旧可以正常的使用。原因是 npm3 将所有的依赖都平铺到node_modules 下,因此 require 函数可以查找到它,这样也给项目带来了一些风险:
阅读困难:没有在 package.json 里定义却可以引入这个包,why??? 引入的版本不确定性:像上面这个例子,App1 require 的 packageB 是 v1.0 版本还是 v2.0 版本完全取决于A C G H的依赖,如果有一天A C G H版本更新依赖 packageBv5.0 版本了,App1 在不知情的情况下依赖的版本从 v1.0 升级到 v5.0,或许会给项目带来意想不到的问题。
Yarn
2016年,Facebook在官网上发布了这篇文章:Yarn: A new package manager for JavaScript 。文章开篇就说了,在使用 npm 的过程中,他们遇到了:一致性、安全性、离线安装和性能方面的问题。
那我们看看yarn是怎么解决的:
一致性&安全性:增加 lockfiles(yarn.lock):记录所有被安装依赖的版本号,安装时将优先参考 lock 文件的提供的版本: 离线安装:每次从网络下载一个依赖包时,yarn都会将其放在本地的全局缓存中,下次下载会优先在全局缓存目录中查找,如果有,将其copy到当前目录下; 性能问题:并行安装。无论是 npm 还是 yarn,在安装时都会执行一系列任务。npm 是按照队列执行每一个 package:当前的package 安装完成后,再去执行下一个package,而 yarn 是同步执行所有的任务。
我们上述的不确定性得到了很好的解决,还顺带提升了安装速度,提供了离线安装等功能。所以当 yarn 一发布,就受到了广泛的关注,当天,npm 官方博客当天发表了一篇Hello, Yarn!。恭喜了 yarn 的开源,并对 yarn 团队及 facebook 为社区及整个 npm 生态做出的贡献给予了很高的评价,后续的 npm v5 也吸纳了 yarn 优秀的 lock 和缓存机制。
至此,包管理机制的大楼看似已经建成,但依旧有两朵乌云飘荡在上空:
多项目之间的复用还是没有解决 隐式依赖
Pnpm
1. 多项目间的复用
如何解决多项目间的复用呢?既然都有了本地缓存,可不可以不要复制一份依赖到项目里呢?
pnpm在解决这个问题上采取了硬链接的方式:硬链接与平时我们使用比较多的软链接不同的是,他会直接指向磁盘中原始文件所在的地址。
在下面这个例子中,App1 和 App2 都依赖了 Bv1.0,并且各自的 node_modules 都占用了 1MB 的空间,那么看起来像是一共占用了2MB 的空间,实际上他们指向的是相同的磁盘空间,所以 Bv1.0 在两个项目里总共也就占用了 1MB,而不是 2MB。
同时,这种方式还很好的解决了上面提到的 “npmv3 不完全的重复依赖” 问题。
由于绝大部分的依赖包都是通过硬链接串联到项目里的node_modules下,节省了网络下载的开销和复制磁盘的开销,这也是pnpm速度一骑绝尘的原因:
2. 同一个依赖多个版本的复用
对于同一个依赖包的不同版本,则仅有版本之间不同的文件会被存储起来。例如:Bv1.0 包含 100 个文件,Bv2.0发布时只有一个文件有修改(version2.js),那么 pnpm 本地缓存时只会缓存 version2.js 到存储中,而不会因为一个文件的修改而保存依赖包的所有文件。
3. 创建非扁平的node_modules目录
如何解决隐式依赖呢,既然打平会让 require 能正确访问哪些它本不该访问的依赖,那我直接不打平不就好了......
在默认情况下,pnpm 的 node_modules 一级目录只存在 package.json 里显式声明的依赖,其他依赖的依赖都放在.pnpm下。
乍一眼看,.pnpm下的目录结构和 npmv3 下的 node_modules 很像,但他每个package下有一个node_modules,打开看里面的结构和 npmv1&v2的目录又有点像,有当前package下所有的依赖,实际上这些都是通过 软连接(符号链接)串联起来的,可以看上面官网给出的示例图。这样既完美杜绝隐式依赖,又能方便的查看当前依赖的目录结构,同时还不会增加存储空间。
4. monorepo 支持
Pnpm 天然支持 monorepo 项目,除了可以指定 workspace,它还提供了很多指令能够方便的对 workspace 下的项目做依赖管理,这里就不多做赘述了。
存在的问题
非扁平化破坏性的结构和必须使用自身锁文件pnpm-lock.yaml,都给迁移带来了写成本; 软连接的兼容性,存在一些不能使用的场景; 不同应用的依赖是硬链接到同一份文件上,如果在调试时修改了某个依赖的文件,可能无意间影响了其他项目。
后记
其实发展到现在,npm,yarn 以及 pnpm 后面都陆陆续续迭代了很多的功能和优化,速度上也越发相似,大家都在互相学习,互相进步,也正是这种追求极致和坦诚清晰的氛围,开源社区才会越来越好的吧~
字节跳动巨量星图/TCM团队持续招聘前端工程师
关于我们
字节商业化旗下巨量星图前端团队,依托抖音/TT丰富的达人生态及产品能力,高效连接创作人与广告主,激发优质创作的营销价值,三年时间从小项目成长为集团独立业务,我们期待你的加入!
我们的业务介绍可以看下面这个视频:
来看看团队
更多信息可以参考我们在知乎上的文章:
字节跳动招聘 | 和优秀的人,做有挑战的事!
https://zhuanlan.zhihu.com/p/106010422
如果你
崇尚自由,和有趣的人一起做欢快的事
热爱前端,热爱技术,追求极致
有自驱力,有责任心
王者荣耀玩家,团队人均王者段位,快来和我们一起上分
欢迎加入我们,我们大量招聘前端工程师,社招/校招/实习生不限,能力层级不限。
工作地点:上海/北京/杭州/山景城
期待那个优秀的你加入大星图团队!
了解更多
如果想了解更多关于业务、团队和岗位的信息,可以加 寸志(过气网红)微信:island205,邮件:cunzhi@bytedance.com,和TL直接沟通!
简历直投
记得备注定向星图大前端团队哦!